# vim: set fileencoding=utf-8 expandtab shiftwidth=4 tabstop=4 softtabstop=4:

import middleware #?

__all__ = [
    'kato',
    'middleware',
]

from sqlalchemy import *
from sqlalchemy import sql, schema, exceptions, util
from sqlalchemy import types as sqltypes
from sqlalchemy.sql import *
from sqlalchemy.exceptions import NoSuchTableError
import sqlalchemy.types
import settings
import datetime
import re, os
import django.utils.simplejson
try:
    import magic
    ms = magic.open(magic.MAGIC_MIME)
    ms.load()
except ImportError:
    ms = None

class KatoRequest:
    def __init__(self, request):
        self.request = request
        self.kato_request = decoder.decode(request.raw_post_data)

    def get_provider(self, rw, ro):
        print self.kato_request
        if self.kato_request['method'] == 'get_primary_keys':
            return PrimaryKeysProvider(self, rw, ro)
        else:
            table = self.kato_request['params'][0]
            if table in rw:
                return self.get_provider_for_table(rw[table])
            elif table in ro:
                if self.kato_request['method'] != 'get_data':
                    return ErrorProvider(self, 'view is read-only: %s' % table)
                return self.get_provider_for_table(ro[table])
            else:
                return ErrorProvider(self, 'no such view: %s' % table)

    def get_provider_for_table(self, table):
        if isinstance(table, (tuple, list)):
            real_table = table[0]
        else:
            real_table = table
        provider = SqlAlchemyRawProvider(self, real_table)
        if isinstance(table, (tuple, list)):
            for f in table[1:]:
                provider = f(self, provider)
        return provider
        

class KatoJSONEncoder(django.utils.simplejson.JSONEncoder):
    def default(self, o):
        # is this the right place to address this issue?
        if isinstance(o, datetime.timedelta):
            return '%d:%02d:%02d' % (o.days*24 + o.seconds/3600, (o.seconds%3600)/60, o.seconds%60)
        try:
            s = django.utils.simplejson.JSONEncoder.default(self, o)
        except TypeError:
            s = str(o)
        return s

def handle_upload(f):
    meta = MetaData(bind=settings.DB_URL)    
    upload = Table('kato_upload', meta,
                   Column('kato_upload_id', Integer, Sequence('kato_upload_kato_upload_id_seq'), primary_key=True),
                   Column('kato_upload_original_name', String),
                   Column('kato_upload_mime', String))
    meta.create_all()
    res = upload.insert().execute()
    newid = res.last_inserted_ids()[0]
    try:
        os.mkdir(settings.KATO_UPLOAD_DIR)
    except:
        pass
    filename = "%s/%d" % (settings.KATO_UPLOAD_DIR, newid)
    destination = open(filename, 'wb+')
    for chunk in f.chunks():
        destination.write(chunk)
    destination.close()
    mime = f.content_type
    if ms is not None:
        mime = ms.file(filename)
    upload.update(upload.c.kato_upload_id == newid).execute(kato_upload_original_name = f.name, kato_upload_mime = mime)
    return newid

def kato(rw={}, ro={}, passthru=False):
    def wrapper(view):
        def wrapper2(request, *args, **kwargs):
            request.kato = None
            try:
                if request.FILES['file']:
                    return django.http.HttpResponse("%d" % handle_upload(request.FILES['file']))
            except Exception, e:
                print e
            if request.META.has_key('HTTP_X_REQUESTED_WITH'):
                request.kato = KatoRequest(request)
                try:
                    request.kato.data_provider = request.kato.get_provider(rw, ro)
                    if passthru:
                        response = view(request, *args, **kwargs)
                    else:
                        response = request.kato.data_provider.get_response()
                except Exception, e:
                    import traceback
                    response = ErrorProvider(request.kato, '<pre>%s</pre>' % traceback.format_exc()).get_response()
                return django.http.HttpResponse(encoder.encode(response))
            else:
                return view(request, *args, **kwargs)
        return wrapper2
    return wrapper

def convert_type(type):
    if isinstance(type, sqlalchemy.types.String):
        return 'text'
    if isinstance(type, sqlalchemy.types.Integer):
        return 'int4'
    if isinstance(type, sqlalchemy.types.DateTime):
        return 'timestamp'
    if isinstance(type, sqlalchemy.types.Numeric):
        return 'numeric'
    return 'text'

class ErrorProvider:
    def __init__(self, kato, msg=''):
        self.msg = msg
        self.kato = kato

    def get_response(self):
        return {
            'id': self.kato.kato_request['id'],
            'error': self.msg,
        }

class PrimaryKeysProvider:
    def __init__(self, kato, rw={}, ro={}):
        self.rw = rw
        self.ro = ro
        self.kato = kato

    def get_response(self):
        result = None
        if self.kato.kato_request['method'] in ('get_primary_keys'):
            result = {}
            result['primary_keys'] = {}
            for d in (self.rw, self.ro):
                for table in d:
                    provider = self.kato.get_provider_for_table(d[table])
                    pk = provider.get_primary_key()
                    if pk:
                        result['primary_keys'][pk] = table
        return {
            'id': self.kato.kato_request['id'],
            'result': result,
        }

class SqlAlchemyRawProvider(object):
    def __init__(self, kato, table):
        self.table = table
        self.engine = self.table.bind
        self.kato = kato
        self.row_owner_restrictions = []

    def get_primary_key(self):
        keys = self.table.primary_key.keys()
        if len(keys) == 1:
            return keys[0]
        else:
            return []

    def get_response(self):
        result = None
        method = self.kato.kato_request['method']
        if method in ('get_data','do_operations'):
            result = self.__class__.__dict__[method](self)
        return {
            'id': self.kato.kato_request['id'],
            'result': result,
        }

    def add_row_owner_restriction(self, column, value):
        self.row_owner_restrictions.append((column, value))

    def get_data(self):
        columns = []
        position = 0
        for column in self.table.c:
            columns += ({
                'num':          position,
                'type':         convert_type(column.type),
                'not null':     not column.nullable,
                'column':       column.name,
                'primary position':
                                column.primary_key,
                                
                'len':          'not yet supported',
                'has default':  column.default,            
            },)
            position += 1

        select = self.table.select()
        criteria = self.kato.kato_request['params'][1]

        for p in criteria:
            g = re.match(r'^([^_]*)_?(.*)$', p)
            type = g.group(1)
            col = g.group(2) if len(g.groups()) > 1 else None
            if col and not col in self.table.c:
                raise Exception("don't fuck with the jedi")
            value = criteria[p]

            # tu jeszcze będzie rzucał wyjątek dla kryteriów w rodzaju 'search' z brakującą kolumną

            if type == 'search':
                select.append_whereclause(self.table.c[col] == value)
            elif type == 'regexp' or type == 'ilike':
                select.append_whereclause(self.table.c[col].like('%'+value+'%'))
            elif type == 'rregexp':
                # this is probably postgresql-specific
                select.append_whereclause(self.table.c[col].op('~')(value))
            elif type == 'ge':
                select.append_whereclause(self.table.c[col] >= value)
            elif type == 'gt':
                select.append_whereclause(self.table.c[col] > value)
            elif type == 'lt':
                select.append_whereclause(self.table.c[col] < value)
            elif type == 'le':
                select.append_whereclause(self.table.c[col] <= value)
            elif type == 'isnull':
                select.append_whereclause((self.table.c[col] == None) if value else (self.table.c[col] != None))
            elif type == 'orderby':
                if col:
                    select.append_order_by(desc(self.table.c[col]) if value.lower() == 'desc' else asc(self.table.c[col]))
                else:
                    for ocol in value:
                        select.append_order_by(asc(self.table.c[ocol]))
            elif type == 'limit':
                select = select.limit(int(value)+1)
            elif type == 'offset':
                select = select.offset(int(value))
            else:
                raise Exception('unrecognized criteria: %s' % p)

        for r in self.row_owner_restrictions:
            select.append_whereclause(self.table.c[r[0]] == r[1])

        for pk in self.table.primary_key.columns:
            select.append_order_by(desc(pk))

        print select
        
        self.connection = self.engine.connect()
        rows = [list(x) for x in self.connection.execute(select)]
        self.connection.close()
        result = {
            'head':     {'revision': 0},
            'columns':  columns,
            'rows':     rows
        }
        return result

    def dict_from_list(self, dict_to_be, style=None):
        dict = {}
        columns = list(self.table.c)
        pk = [x for x in columns if x.primary_key]
        composite_pk = (len(pk) > 1)
        no_primary_key = pk == []    # XXX
        for i in range(0,len(dict_to_be)):
        
            if style in ('update','insert') and not composite_pk:
                if columns[i].primary_key or (no_primary_key and i==0):
                    continue
            
            if style in ('where',):
                if not columns[i].primary_key and (not no_primary_key or i>0):
                    continue
            
            # default:
            
            if columns[i].name == 'django_logged_in':
                role = self.kato.request.user.groups.all()[0].name
                if role == 'zlecenie':
                    dict[columns[i].name] = 'Klient'
                else:
                    dict[columns[i].name] = "%s %s" % (self.kato.request.user.first_name, self.kato.request.user.last_name)
            elif columns[i].name == 'django_logged_in_id':
                dict[columns[i].name] = self.kato.request.user.id
            else:
                dict[columns[i].name] = dict_to_be[i]

        return dict
    
    def do_operations(self):
        self.connection = self.engine.connect()
        trans = self.connection.begin()
        params = self.kato.kato_request['params']
        last_result = None
        for operation in params[1]:
            if operation['command'] in ('insert', 'update', 'delete'):
                last_result = self.__class__.__dict__['do_operation_' + operation['command']](self, operation['row'])
        if 'try_only' in params[2] and params[2]['try_only']:
            trans.rollback()
        else:
            trans.commit()
        self.connection.close()
        return last_result

    def make_where(self, row):
        where = self.dict_from_list(row, 'where')
        return and_(*[self.table.c[x[0]]==x[1] for x in where.items()])
        
    def do_operation_update(self, row):
        v = self.dict_from_list(row, 'update')
        for r in self.row_owner_restrictions:
            v[r[0]] = r[1]
        update = self.table.update(self.make_where(row), values=v)
        for r in self.row_owner_restrictions:
            update = update.where(self.table.c[r[0]] == r[1])
        print update
        self.connection.execute(update)

    def do_operation_insert(self, row):
        v = self.dict_from_list(row, 'insert')
        for r in self.row_owner_restrictions:
            v[r[0]] = r[1]
        insert = self.table.insert(values = v)
        print insert
        result = self.connection.execute(insert)
        return result.last_inserted_ids()

    def do_operation_delete(self, row):
        delete = self.table.delete(self.make_where(row))
        for r in self.row_owner_restrictions:
            delete = delete.where(self.table.c[r[0]] == r[1])
        print delete
        self.connection.execute(delete)

decoder = django.utils.simplejson.JSONDecoder()
encoder = KatoJSONEncoder()
